"""数据管理器实例"""
import ast
import importlib
import json
import os
import sys
from filelock import FileLock
from datetime import datetime as datetime_
import pandas as pd
from core.map_const import DataFolderDataCategoryMap
from core.constant import *
from core.object import DataDescribe, RunStatus
from conf import conf
from module.logic_module.log_engine.model import LogEngineModel
import queue
from mdutils.mdutils import MdUtils
import markdown
import webbrowser
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
from tools.file_manager import merge_columns, data_df_interval, file_info_log_str


class FileManagerModel:
    def __init__(self, master):
        self.master = master
        # 项目相关路径
        self.project_path = os.getcwd()
        self.conf_path = os.path.join(self.project_path, "conf", "conf.json")
        self.support_path = os.path.join(self.project_path, "support")
        # 设置全局日志通道
        self.log_queue = queue.Queue()
        # 获取配置信息
        self.conf_dc = self.read_json(self.conf_path)

        # 当前动态模块
        self.now_dynamic_module = None
        # =============
        self.change_now_module(conf.Framework.default_module.value)

        # 获取当前model信息
        now_model_num = self.conf_dc["now_model_num"]
        now_model_name = ''.join([conf.Framework.model_prefix.value, str(now_model_num)])
        self.now_model_num = None
        self.now_model_name = now_model_name
        # 模型相关路径
        self.model_path = None

        self.data_path = None
        self.market_live_data_path = None
        self.market_factor_live_data_path = None
        self.market_factor_signal_live_data_path = None
        self.market_data_path = None
        self.market_factor_data_path = None
        self.market_factor_signal_data_path = None
        self.action_data_path = None
        self.temp_data_path = None

        self.result_path = None
        self.factor_am_path = None
        self.strategy_path = None

        self.researcher_path = os.path.join(self.project_path, "module", "logic_module", "researcher")
        self.backtest_summary_path = os.path.join(self.researcher_path, "backtest_summary.py")
        self.factor_summary_path = os.path.join(self.researcher_path, "factor_summary.py")

        # 当前项目下所有model
        # 项目所需要的动态信息
        self.data_def_group_dc = None
        # 所有文件详细描述信息
        self.describe_all_dc: dict[str, DataDescribe] = None

        # 初始化日志引擎
        self.log_engine = None

        # =============
        self.change_now_model(self.now_model_name)

        # 初始化运行状态实例
        self.run_status = RunStatus(self.master)
        self.run_status.row1st = "运行中的模块："

        # 回测结果相应文件夹
        self.bt_result_dir_name = ""
        # 因子分析结果文件夹
        self.fa_result_dir_name = ""
        # fighting模块结果文件夹
        self.fht_lm_result_dir_name = ""
        self.fht_hm_result_dir_name = ""
        self.fht_ls_result_dir_name = ""
        self.fht_hs_result_dir_name = ""
        self.fht_lr_result_dir_name = ""

    def init(self, master):
        self.__init__(master)

    def change_now_model(self, model_name):
        self.now_model_num = int(model_name.split(Model.MODEL_PRE.value)[1])
        self.now_model_name = model_name
        self.model_path = os.path.join(self.project_path, "model", self.now_model_name)
        # 检查model folder是否存在
        if os.path.exists(self.model_path):
            pass
        else:
            info = f"模型：{model_name}不存在，自动配置编号最大的模型为当前模型。"
            print(info)
            # 获取路径下编号最大的模型
            model_dir_path = os.path.dirname(self.model_path)
            model_name_ls = [f for f in os.listdir(model_dir_path) if os.path.isdir(os.path.join(model_dir_path, f)) and "spore" not in f]
            model_num_ls = []
            for model_name in model_name_ls:
                if model_name.split("model")[1].isdigit():
                    model_num_ls.append(int(model_name.split("model")[1]))
            # 找出编号最大的model
            max_num = max(model_num_ls)
            self.now_model_num = max_num
            self.now_model_name = "model" + str(self.now_model_num)
            self.model_path = os.path.join(self.project_path, "model", self.now_model_name)
        # 获取配置信息并修改
        self.conf_dc = self.read_json(self.conf_path)
        self.conf_dc["now_model_num"] = self.now_model_num
        self.renew_json(self.conf_dc, self.conf_path)
        # 相关路径改变
        self.data_path = os.path.join(self.model_path, "data")
        self.market_live_data_path = os.path.join(self.data_path, DataFolder.MkLive.value)
        self.market_factor_live_data_path = os.path.join(self.data_path, DataFolder.MkFtLive.value)
        self.market_factor_signal_live_data_path = os.path.join(self.data_path, DataFolder.MkFtSgLive.value)
        self.market_data_path = os.path.join(self.data_path, DataFolder.Mk.value)
        self.market_factor_data_path = os.path.join(self.data_path, DataFolder.MkFt.value)
        self.market_factor_signal_data_path = os.path.join(self.data_path, DataFolder.MkFtSg.value)
        self.action_data_path = os.path.join(self.data_path, DataFolder.Action.value)
        self.temp_data_path = os.path.join(self.data_path, DataFolder.Temp.value)

        self.result_path = os.path.join(self.model_path, "result")
        self.factor_am_path = os.path.join(self.model_path, "factor", "factor_am.py")
        self.strategy_path = os.path.join(self.model_path, "strategy")

        # 更新data_def_group文件
        self.gen_data_def_group()

        # 更新describe文件
        self.gen_describe()

    def change_now_module(self, module: Module):
        self.now_dynamic_module = module

        pass

    def config_log_engine(self):
        text_widget_dc = {
            LogName.Running: self.master.main_window_view.running_view.text,
            LogName.Operate: self.master.main_window_view.operate_view.text,
            LogName.FileAction: self.master.main_window_view.file_action_view.text,
            LogName.FileInfo: self.master.main_window_view.file_info_view.text,
            LogName.Operate_run_status: self.master.main_window_view.operate_view.run_status_text
        }
        self.log_engine = LogEngineModel(self, text_widget_dc, self.log_queue)
        # 初次配置日志引擎，若存在描述信息则打印一次
        if self.describe_all_dc is not None:
            info = file_info_log_str(self.describe_all_dc)
            self.log_engine.emit(info, LogName.FileInfo)

    def renew_json(self, renew_dc, json_path):
        # 将renew_dc中的键值去更新json_path的相应键值，原有内容保持不变
        json_data = self.read_json(json_path)
        for key, value in renew_dc.items():
            json_data[key] = value

        self.save_json(json_data, json_path)

    def gen_data_def_group(self):
        # 初始化 ----------------------------------
        m_live_data_name_ls = [conf.DataBridge.own_data_name.value]  # 当前模型下含行情的实时数据名
        mf_live_data_name_ls = [conf.DataBridge.own_data_name.value]  # 当前模型下含行情、因子的实时数据名
        mfs_live_data_name_ls = [conf.DataBridge.own_data_name.value]  # 当前模型下含行情、因子及信号的实时数据名
        m_data_name_ls = []  # 当前模型下含行情的历史数据名
        mf_data_name_ls = []  # 当前模型下含行情、因子的历史数据名
        mfs_data_name_ls = []  # 当前模型下含行情、因子及信号的历史数据名

        bt_strategy_name_ls = []  # 当前模型下的策略名
        ddq_message_name_ls = []  # 当前模型下的策略名
        ddq_simulate_name_ls = []  # 当前模型下的策略名
        ddq_real_name_ls = []  # 当前模型下的策略名
        ddq_machine_learning_engine_name_ls = []  # 当前模型下的机器学习引擎类名

        backtest_summary_ls = []  # 当前回测的分析方案名
        factor_summary_ls = []  # 当前因子的分析方案名
        factor_name_ls = []  # 当前模型下的可用因子名

        # 编辑当前项目的信息并存储至data_def_group.json
        # 遍历文件夹及其子文件夹
        for root, dirs, files in os.walk(self.data_path):
            for file in files:
                # 检查文件扩展名是否为 .csv
                if file.endswith(".csv") and os.path.isfile(os.path.join(root, file)):
                    if "Live" in file:
                        if file.startswith(DataCategory.MkLive.value):
                            m_live_data_name_ls.append(file)
                        elif file.startswith(DataCategory.MkFtLive.value):
                            mf_live_data_name_ls.append(file)
                        elif file.startswith(DataCategory.MkFtSgLive.value):
                            mfs_live_data_name_ls.append(file)
                        else:
                            pass
                    else:
                        if file.startswith(DataCategory.Mk.value):
                            m_data_name_ls.append(file)
                        elif file.startswith(DataCategory.MkFt.value):
                            mf_data_name_ls.append(file)
                        elif file.startswith(DataCategory.MkFtSg.value):
                            mfs_data_name_ls.append(file)
                        else:
                            pass

        # 获取策略名放至列表
        bt_strategy_name_ls = [f for f in os.listdir(self.strategy_path) if "bt_strategy" in f]
        ddq_message_name_ls = [f for f in os.listdir(self.strategy_path) if "ddq_message" in f]
        ddq_simulate_name_ls = [f for f in os.listdir(self.strategy_path) if "ddq_simulate" in f]
        ddq_real_name_ls = [f for f in os.listdir(self.strategy_path) if "ddq_real" in f]
        ddq_machine_learning_engine_name_ls = [f for f in os.listdir(self.strategy_path) if "ddq_ml" in f]

        # 获取研究方案放至列表
        backtest_summary_ls = self.get_function_names(
            os.path.join(self.researcher_path, "backtest_summary.py"))  # 当前回测的分析方案
        factor_summary_ls = self.get_function_names(
            os.path.join(self.researcher_path, "factor_summary.py"))  # 当前因子的分析方案
        fighting_summary_ls = self.get_function_names(
            os.path.join(self.researcher_path, "fighting_summary.py"))  # 当前因子的分析方案

        # 获取备选的所有因子
        factor_name_ls = self.get_factor_names()

        # 综合
        # 此处的data_def_group应是字段完整的
        self.data_def_group_dc = {DataDefGroup.MkLive.value: m_live_data_name_ls,
                                  DataDefGroup.MkFtLive.value: mf_live_data_name_ls,
                                  DataDefGroup.MkFtSgLive.value: mfs_live_data_name_ls,
                                  DataDefGroup.Mk.value: m_data_name_ls,
                                  DataDefGroup.MkFt.value: mf_data_name_ls,
                                  DataDefGroup.MkFtSg.value: mfs_data_name_ls,
                                  DataDefGroup.BTSName.value: bt_strategy_name_ls,
                                  DataDefGroup.DDQMName.value: ddq_message_name_ls,
                                  DataDefGroup.DDQSName.value: ddq_simulate_name_ls,
                                  DataDefGroup.DDQRName.value: ddq_real_name_ls,
                                  DataDefGroup.DDQMLEName.value: ddq_machine_learning_engine_name_ls,
                                  DataDefGroup.BSummary.value: backtest_summary_ls,
                                  DataDefGroup.FSummary.value: factor_summary_ls,
                                  DataDefGroup.FHTSummary.value: fighting_summary_ls,
                                  DataDefGroup.FName.value: factor_name_ls}
        return self.data_def_group_dc

    def get_factor_names(self):
        from tools.framework import load_module_from_path, get_method_from_class

        factor_am_py = load_module_from_path(self.factor_am_path)
        factor_ins = factor_am_py.ArrayManagerFactor(size=1, symbol='', interval=Interval.MINUTE)
        if factor_ins.private_factor_ls:
            factor_name_ls = factor_ins.private_factor_ls
        else:
            factor_name_ls = get_method_from_class(factor_am_py.ArrayManagerFactor)
        for name in factor_ins.public_factor_ls:
            factor_name_ls.append(name)
        # 删除非因子方法名
        factor_name_ls = [x for x in factor_name_ls if x != "get_public_factor"]

        return factor_name_ls

    def get_function_names(self, file_path):
        with open(file_path, 'r', encoding='utf-8') as file:
            node = ast.parse(file.read())
            function_names = [n.name for n in ast.walk(node) if isinstance(n, ast.FunctionDef) and "plan" in n.name]

            return function_names

    def gen_describe(self):
        category_ls = [DataFolder.MkLive, DataFolder.MkFtLive, DataFolder.MkFtSgLive,
                       DataFolder.Mk, DataFolder.MkFt, DataFolder.MkFtSg]
        describe_all_dc = {}
        for category in category_ls:
            sub_data_path = os.path.join(self.data_path, category.value)
            for f in os.listdir(sub_data_path):
                if f.endswith('.csv') and os.path.isfile(os.path.join(sub_data_path, f)):
                    data_path = os.path.join(sub_data_path, f)
                    data_category = DataFolderDataCategoryMap[category]
                    describe_all_dc[f] = self.data_describe(data_path, data_category)
        # 此处的describe_dc应是字段完整的
        self.describe_all_dc = describe_all_dc
        info = file_info_log_str(self.describe_all_dc)
        if self.log_engine is not None:
            self.log_engine.emit(info, LogName.FileInfo)

    def data_describe(self, path, category: DataCategory):
        # 获取公共信息
        data_df_dc: dict = self.read_dc_csv(path)
        # 取字典第一个键对应值来计算大部分描述信息
        # symbol字段值
        symbol_ls = list(data_df_dc.keys())
        data_df = data_df_dc[symbol_ls[0]]
        if len(data_df) == 0:
            data_describe_ins = DataDescribe(name=os.path.basename(path),
                                             interval_val=None,
                                             path=path,
                                             symbol_ls=symbol_ls,
                                             merge_columns_ls=data_df_dc[Model.DefaultSymbol.value].columns.tolist(),
                                             data_len=0,
                                             data_modified_time=None,
                                             start_end_time_ls=None,
                                             category=category)
            return data_describe_ins
        # 数据字段值
        data_columns_ls = data_df.columns.tolist()
        # 需要用简写合并显示的字段
        merge_columns_ls = merge_columns(data_columns_ls)
        # 数据长度
        data_len = data_df.__len__()
        # 从第一个symbol中获取文件相应的Interval
        interval_val = data_df_interval(data_df)

        # 数据修改日期
        # 获取文件的修改时间戳
        modified_timestamp = os.path.getmtime(path)

        # 将时间戳转换为日期格式
        data_modified_time = datetime_.fromtimestamp(modified_timestamp)
        # 数据起止时间
        start_time: datetime_ = data_df.index[0]
        end_time: datetime_ = data_df.index[-1]
        start_end_time_ls = [start_time, end_time]

        data_describe_ins = DataDescribe(name=os.path.basename(path),
                                         interval_val=interval_val,
                                         path=path,
                                         symbol_ls=symbol_ls,
                                         merge_columns_ls=merge_columns_ls,
                                         data_len=data_len,
                                         data_modified_time=data_modified_time,
                                         start_end_time_ls=start_end_time_ls,
                                         category=category)
        return data_describe_ins

    def read_csv(self, path) -> dict:
        """仅用于读取时间序列的csv文件，且数据以时间为索引"""
        lock = FileLock(path+".lock")
        with lock:
            try:
                data = pd.read_csv(path)
            except Exception as e:
                print(e)
                print("使用gbk编码继续尝试。")
                data = pd.read_csv(path, encoding='gbk')

        if "datetime" in data.columns:
            # 将时间作为索引
            data = data.set_index("datetime", drop=True)
            data.index = pd.to_datetime(data.index)
        elif '' in data.columns:
            data = data.set_index('', drop=True)
            data.index = pd.to_datetime(data.index)
            data.index.name = "datetime"
        else:
            pass

        return data

    def read_dc_csv(self, path) -> dict:
        from tools.file_manager import split_df_by_symbol
        """仅用于读取时间序列的csv文件，且数据以时间为索引"""
        if os.path.exists(path):
            lock = FileLock(path + ".lock")
            with lock:
                data = pd.read_csv(path)
            # 将时间作为索引
            if "datetime" in data.columns:
                data = data.set_index('datetime', drop=True)
            elif '' in data.columns:
                data = data.set_index('', drop=True)
                data.index.name = "datetime"
            elif "Unnamed: 0" in data.columns:
                data = data.set_index("Unnamed: 0", drop=True)
                data.index.name = "datetime"
            else:
                raise ValueError("索引设置异常。")
            temp = str(data.index[0]).isdigit()
            if not temp:
                data.index = pd.to_datetime(data.index)
            else:
                data.index = pd.to_datetime(data.index, format="%Y%m%d")
            data_split = split_df_by_symbol(data)
            return data_split
        else:
            return False

    def save_csv(self, data: pd.DataFrame, path):
        self.mk_dir(os.path.dirname(path))
        if data is not None:
            data.index.name = "datetime"
            data.to_csv(path)
            info = f"新csv文件生成，路径：{path}。"
        else:
            info = f"数据为None，未能保存。"

        self.log_engine.emit(info, LogName.FileAction)

        # 更新data_def_group文件
        self.gen_data_def_group()
        # 更新describe文件
        self.gen_describe()

    def save_dc_csv(self, data_dc: dict[str, pd.DataFrame], path):
        self.mk_dir(os.path.dirname(path))
        combined_df = pd.DataFrame()
        for df_name, df in data_dc.items():
            df["symbol"] = df_name
            combined_df = pd.concat([combined_df, df])
        combined_df = combined_df.sort_index()
        if not combined_df.index.name:
            combined_df.index.name = "datetime"
        combined_df.to_csv(path)
        info = f"新csv文件生成，路径：{path}"
        self.log_engine.emit(info, LogName.FileAction)

        # 更新data_def_group文件
        self.gen_data_def_group()
        # 更新describe文件
        self.gen_describe()

    @staticmethod
    def read_json(path):
        """输入路径读取json文件，返回对应dict"""
        lock = FileLock(path+".lock")
        with lock:
            with open(path, 'r', encoding='utf-8') as file:
                dc = json.load(file)
        return dc

    @staticmethod
    def save_json(data, path):
        lock = FileLock(path+".lock")
        with lock:
            with open(path, 'w', encoding='utf-8') as file:
                json.dump(data, file, ensure_ascii=False, indent=4)
        return data

    @staticmethod
    def load_model(path):
        """输入路径读取ml_model文件，返回对应机器学习模型"""
        pass

    @staticmethod
    def save_model(ml_model, path):
        """输入路径保存ml_model文件"""
        pass

    def save_mpl_fig(self, plt, path):
        # 预检查文件夹
        self.mk_dir(os.path.dirname(path))
        plt.savefig(path)
        info = f"新matplotlib图像文件生成，路径：{path}"
        self.log_engine.emit(info, LogName.FileAction)

    def save_md(self, md_utils: MdUtils):
        # 保存md文件
        md_utils.create_md_file()
        info = f"新的md文件生成，路径：{md_utils.file_name}"
        self.log_engine.emit(info, LogName.FileAction)

        pass

    def md2html(self, md_file_path, is_open=False):
        # 定义转换后的 HTML 文件的路径
        html_file_path = md_file_path.replace(".md", ".html")

        # 转为html文件
        # 读取 Markdown 文件内容
        with open(md_file_path, 'r', encoding='utf-8') as f:
            md_content = f.read()
        # 使用 markdown 库将 Markdown 转换为 HTML
        html_content = markdown.markdown(md_content, extensions=["markdown.extensions.tables"])
        # 将 HTML 内容写入到 HTML 文件
        with open(html_file_path, 'w', encoding='utf-8') as f:
            f.write(html_content)

        if is_open:
            info = f"即将通过系统默认浏览器打开html文件，路径：{html_file_path}"
            self.log_engine.emit(info, LogName.FileAction)
            # 打开html文件
            # 使用 webbrowser 库打开 HTML 文件
            webbrowser.open('file://' + os.path.realpath(html_file_path))

        pass

    def show_img(self, img_path):
        # 使用 Matplotlib 读取 JPG 图像
        img = mpimg.imread(img_path)

        # 显示图像
        plt.imshow(img)
        plt.axis('off')  # 不显示坐标轴
        plt.show()

    def mk_dir(self, dir_path):
        # 每次创建文件前预创建所需的文件夹，若不存在则创建，若已存在则预先给出弹窗提示，通过后再清空其中内容，否则用户修改文件夹名称。
        if not os.path.exists(dir_path):
            os.makedirs(dir_path)
            info = f"创建新文件夹，路径{dir_path}"
            self.log_engine.emit(info, LogName.FileAction)
        else:
            pass

    def copy_file(self, old_path, new_path):
        import shutil
        try:
            shutil.copy2(old_path, new_path)
            info = f"复制文件成功，目标路径{new_path}"
        except:
            info = f"复制文件失败，目标路径{new_path}"

        self.log_engine.emit(info, LogName.FileAction)
        pass

    def delete_file(self, path):
        try:
            os.remove(path)
            info = f"删除文件成功，路径{path}"

        except:
            info = f"删除文件失败，路径{path}"

        self.log_engine.emit(info, LogName.FileAction)
        pass

    def return_strategy(self, strategy_name):
        # 返回策略文件模块
        sys.path.append(self.strategy_path)
        strategy = importlib.import_module(strategy_name)
        sys.path.remove(self.strategy_path)
        return strategy
